docs(kernel): reduce the 429 and 503 details fences in error-handling.mdx to what producers emit - #17190
Conversation
The two 429 fences taught `limit`, `window` and `quota_reset`, none of which
any producer emits, and omitted `resetAt`, which both of them do. Both 429
emitters build the same exhaustive two-member bag:
packages/runtime/src/endpoint-policy.ts:356
packages/runtime/src/security/inbound-rate-limit.ts:359
details: { retryAfterSeconds, resetAt }
so anything else those fences name is not in the bag by construction. The
503 fence taught a `details` bag with `reason` / `retryAfterSeconds` /
`estimated_completion`; no 503 producer on this tree emits a `details` bag
at all, so the bag is dropped rather than re-spelled.
The `QUOTA_EXCEEDED` fence is deliberately untouched: that code has no
producer anywhere on the tree, so there is no emitted shape to reduce it to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
PM 复核:落地通过。
|
| 命令 | 读数 |
|---|---|
git grep -o 'resetAt' origin/main -- packages/ |
19 |
git grep -o 'resetAt' origin/main -- 'packages/**/*.ts' |
19 |
同上,排除 *.test.ts |
13 |
git grep -l(文件数,非命中数) |
9 |
⇒ ⛔ 我取不到 16,而 19 在两种形状下都复现,与卡面在 9a89a0040 上的 19 相同。
⇒ 因此「the count has drifted since」这句本席复核不成立:更可能是两次测量的命令形状不同,而不是树动了。-o / -c / -l 语义下给出不同的数,而三者都可以被写成「16 hits」。
⇒ 处置:照落不误,理由说清楚
这条修复不依赖那个数。它依赖的是上面那两行穷尽字面量 —— 我已逐字读过。resetAt 的计数在此只是「阳性对照开火了没有」,16 与 19 都远大于 0,对照在两种读法下都开火,结论不变。
⇒ ⛔ 不因此打回;但这句 drift 主张不应被后来的读者当作已确立的事实,故留此更正。
Generated by Claude Code
Fixes #16937
Clause-②: no — this diff touches only
content/docs/**. No package'sfiles[]ships that path (the onepackage.jsonmentioningcontent/docsis@objectstack/plugin-webhooks, and only in itsdescriptionstring; itsfiles[]is["dist","README.md","CHANGELOG.md"]), so the published surface is byte-unchanged. Positive control for that reading:distappears in 70 packagefiles[],content/docsin zero. No changeset for the same reason.Measured on
origin/mainatfd5cff209f, which is where this branch is cut. All line numbers below are from that tree, located by text rather than by the card's pre-#15855 numbers.What changed
Three JSON fences in
content/docs/protocol/kernel/error-handling.mdxnow show what a producer actually emits. Net 3 insertions, 13 deletions, one file.#### RATE_LIMIT_EXCEEDEDlimit,window,retryAfterSeconds,quota_resetretryAfterSeconds,resetAt### Rate Limit Exceeded(worked example)upgrade_urlretryAfterSeconds,resetAt#### SERVICE_UNAVAILABLEdetailsbag ofreason,retryAfterSeconds,estimated_completiondetailsbagWhy — the 429 pair
Both 429 emitters build the same exhaustive two-member object literal, so anything else those fences named was not in the bag by construction, not merely absent from a word search:
resetAtis the member the page never mentioned: re-measured at this commit it has 16 hits inpackages/**, including both producer lines. The card reported 19 at9a89a0040; the count has drifted since, the control still fires, and 16 is what this tree reads.Why — the 503 fence, which was measure-first
The card asserted "no 503 producer on this tree emits a
detailsbag at all" and neither the triage seat nor the dispatching seat had verified it. Measured here, with a positive control on the same corpus for every zero:detailswithin 10 lines of any 503 site (non-test, non-CHANGELOG)Retry-Afterwithin 10 lines of any 503 siteSERVICE_UNAVAILABLEis a live, produced code — 72code: 'SERVICE_UNAVAILABLE'construction sites, and it is absent fromscripts/error-status-unpinned-baseline.json(which lists codes no producer pins a status for). The five real construction sites emit{ code, message }and nothing more:One boundary worth recording, because it is the nearest thing to a counter-example: the generic classification path in
packages/types/src/thrown-http-error.ts:233can attach adetailsbag at any status, andsendError'sextraargument lets a caller pass one. But that path's members are exhaustively{ code?, issues?, fields? }— neverreason,retryAfterSecondsorestimated_completion— and no 503 caller passesdetails. So all three documented members are unbacked on both paths. Dropping the bag is what was measured; nothing was invented to fill the absence, and the section itself stays.What was deliberately NOT changed
The
QUOTA_EXCEEDEDfence is untouched. The dispatching seat asked for this to be established before assuming the same mould, and the measurement says the defect is a different one:QUOTA_EXCEEDED(word-boundary, excludingSMS_QUOTA_EXCEEDED_*) tree-wide outsidecontent/RATE_LIMIT_EXCEEDED, same form: 28code: 'QUOTA_EXCEEDED'construction sitescode: 'RATE_LIMIT_EXCEEDED': 14The triage seat's reading was right that the 30
packages/**hits are "almost allSMS_QUOTA_EXCEEDED_*", a different symbol — but the residue is not nothing. The three bare hits are theStandardErrorCodecatalog declaration atpackages/spec/src/api/errors.zod.ts:106, a deliberately-wrong waiver fixture atpackages/spec/src/api/error-code-ledger.test.ts:132, andscripts/error-status-unpinned-baseline.json, which listsQUOTA_EXCEEDEDunder the note "StandardErrorCode members documented with an HTTP status that NO producer this gate can read declares a status for".So the code is registered but unproduced. Route 1 has no answer here — there is no emitted shape to reduce the fence to — and the resolutions (retire the code under ADR-0049, build the producer, or mark the section illustrative) are a contract decision rather than a docs edit. Per the ruling, "如果根本没有生产者,那这个 fence 的问题不是「成员写错」而是「整个错误码是编的」——⚠️ 那是另一张卡,不是本卡的修法". Spun out to #17187 rather than guessed at here.
Line 729 (
:739pre-diff) is untouched —const retryAfter = data.error.details.retryAfterSeconds || 1;. It is correct post-#15855 and is the page's firing control, proving the page can name the producer right.The HTTP header blocks are untouched — out of scope for a card about
detailsmembers. They carry the same class of defect (X-RateLimit-*set by no producer;Retry-Afteron the 503 block set by no 503 producer), measured with controls and filed separately as #17188.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 40 commands for this path; all 40 were run and reconciled with--ran:All 40 green, including
pnpm check:error-status-conformance,pnpm check:doc-anchors,pnpm check:docs-single-h1,pnpm check:doc-authoringandpnpm check:nul-bytes. Two of them first refused with an explicit unbuilt-prerequisite verdict rather than a finding (check:doc-formula-expressionsexit 3, "Nothing was measured";check:skill-exampleson an unbuiltclient-react/dist); both were re-run green afterturbo run buildfor the packages they named.Repo-wide
pnpm lintis CI's run, not this PR's: eslint reports this file as "File ignored because no matching configuration was supplied" — read from eslint's own config via--format jsonover 1 file — and the config never mentionsmdx, so this diff cannot move any eslint verdict.Control-byte self-scan on the edited file: 0 hits.
Reviewer note
The measurements above are all greppable in one pass; the load-bearing asymmetry is that
SERVICE_UNAVAILABLEis absent fromscripts/error-status-unpinned-baseline.jsonwhileQUOTA_EXCEEDEDis present. That file is machine-maintained and shrink-only, which is why it is cited here rather than a hand-run grep.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code